package org.ytu.classifier.algorithm;

import org.apache.commons.math.complex.Complex;
import org.apache.commons.math.transform.FastFourierTransformer;
import org.ytu.classifier.interfaces.MFCC;
import org.ytu.classifier.util.MathUtil;
import org.ytu.classifier.wavApi.WavFileIO;

import java.util.ArrayList;
import java.util.List;

public class SoundClassifier extends WavFileIO implements MFCC {
   private int M = 205;
   private int N = 512;//Bir frame'deki sample sayısı
   private int cepstralCoefficients;
   private int numberOfFilters;
   private final int samplingRate = 11025;
   private double lowestFilterFreq = 80.0;
   private boolean isMel;
   int[] fbins;

   public SoundClassifier(int cepstralCoefficients, int numberOfFilters, boolean isMel) {
      this.cepstralCoefficients = cepstralCoefficients;
      this.numberOfFilters = numberOfFilters;
      this.isMel = isMel;
   }

   public List<List<Double>> startAlgorithm(String filename) {
      return mfcc(filename);
   }

   public List<List<Double>> mfcc(String fileName) {
      List<Double> wavData = readFrames(fileName);
      wavData = preEmphasis(wavData);
      List<List<Double>> frameList = framing(wavData);
      hamming(frameList);
      fft(frameList);
      fbins = fftBinIndices();
      List<List<Double>> melBankList = melFilterBank(frameList);
      List<List<Double>> transformedList = nonLinearTransformation(melBankList);
      return cepCoefficients(transformedList);
   }

   public List<Double> preEmphasis(List<Double> wavData) {
      List<Double> preEmphasised = new ArrayList<Double>(wavData.size() - 1);
      for (int n = 1; n < wavData.size(); n++) {
         preEmphasised.add(wavData.get(n) - 0.95 * wavData.get(n - 1));
      }
      return preEmphasised;
   }

   //wav dosyası verileri frame'lerine ayrılıyor
   public List<List<Double>> framing(List<Double> wavData) {
      List<List<Double>> frameList = new ArrayList<List<Double>>();

      int startPoint = 0;
      while (startPoint + N - 1 < wavData.size()) {
         List<Double> frame = new ArrayList<Double>(N);
         for (int i = startPoint; i < startPoint + N; i++) {
            frame.add(wavData.get(i));
         }
         frameList.add(frame);
         startPoint += N - M;
      }
      return frameList;
   }

   public void hamming(List<List<Double>> hammingList) {
      for (List<Double> window : hammingList) {
         for (int n = 0; n < N; n++) {
            Double hammingWindow = 0.54 - 0.46 * Math.cos((2 * Math.PI * n) / N - 1);
            window.set(n, hammingWindow * window.get(n));
         }
      }
   }

   //FFT ile time domain'den frequency domain'e geçiliyor
   public void fft(List<List<Double>> fftList) {
      FastFourierTransformer fft = new FastFourierTransformer();
      for (int index = 0; index < fftList.size(); index++) {
         List<Double> oneFrame = fftList.get(index);
         double[] frame = MathUtil.toPrimitiveDouble(oneFrame);
         Complex[] fftFrameComplex = fft.transform(frame);
         //FFT yapılmış Complex dizisi Double listesine dönüştürülüyor
         List<Double> fftFrame = magnitudeSpectrum(fftFrameComplex);
         //FFT yapılmış frame frame listesine ekleniyor
         fftList.set(index, fftFrame);
      }
   }

   public int[] fftBinIndices() {
      int cbin[] = new int[numberOfFilters + 2];

      cbin[0] = (int) Math.round(lowestFilterFreq / samplingRate * N);
      cbin[cbin.length - 1] = N / 2;
      for (int i = 1; i <= numberOfFilters; i++) {
         double fc = isMel ? centerMelFreq(i) : centerFreqBark(i);
         cbin[i] = (int) Math.round(fc / samplingRate * N);
      }
      return cbin;
   }

   public double centerMelFreq(int i) {
      double mel[] = new double[2];

      mel[0] = freqToMel(lowestFilterFreq);
      //mel[1] = freqToMel(samplingRate / 2);
      mel[1] = freqToMel(1000);

      double center = mel[0] + ((mel[1] - mel[0]) / (numberOfFilters + 1)) * i;
      //Mel'den frekansa geçis
      return melToFreq(center);
   }

   public double centerFreqBark(int i) {
      double mel[] = new double[2];

      mel[0] = freqToBark(lowestFilterFreq);
      mel[1] = freqToBark(samplingRate / 2);

      double center = mel[0] + ((mel[1] - mel[0]) / (numberOfFilters + 1)) * i;
      //Mel'den frekansa geçis
      return barkToFreq(center);
   }

   public List<List<Double>> melFilterBank(List<List<Double>> frameList) {
      List<List<Double>> melBankList = new ArrayList<List<Double>>();
      for (List<Double> oneFrame : frameList) {
         double temp[] = new double[numberOfFilters + 2];
         for (int k = 1; k <= numberOfFilters; k++) {
            double num1 = 0, num2 = 0;
            for (int i = fbins[k - 1]; i <= fbins[k]; i++) {
               num1 += ((i - fbins[k - 1] + 1) / (fbins[k] - fbins[k - 1] + 1)) * oneFrame.get(i);
            }
            for (int i = fbins[k] + 1; i <= fbins[k + 1]; i++) {
               num2 += (1 - ((i - fbins[k]) / (fbins[k + 1] - fbins[k] + 1))) * oneFrame.get(i);
            }
            temp[k] = num1 + num2;
         }

         List<Double> melBank = new ArrayList<Double>(numberOfFilters);
         for (int i = 0; i < numberOfFilters; i++) {
            melBank.add(temp[i + 1]);
         }
         melBankList.add(melBank);
      }
      return melBankList;
   }

   public List<List<Double>> nonLinearTransformation(List<List<Double>> melBankList) {
      List<List<Double>> transformedList = new ArrayList<List<Double>>();
      for (List<Double> mel : melBankList) {
         List<Double> transformed = new ArrayList<Double>();
         for (Double data : mel) {
            Double transformedData = Math.log(data);
            if (transformedData < -50.0) {
               transformedData = -50.0;
            }
            transformed.add(transformedData);
         }
         transformedList.add(transformed);
      }
      return transformedList;
   }

   public List<List<Double>> cepCoefficients(List<List<Double>> transformedList) {
      List<List<Double>> resultList = new ArrayList<List<Double>>();
      for (List<Double> transformed : transformedList) {
         List<Double> cepc = new ArrayList<Double>(cepstralCoefficients);
         for (int i = 0; i < cepstralCoefficients; i++) {
            Double data = 0.0;
            for (int j = 1; j <= numberOfFilters; j++) {
               data += transformed.get(j - 1) * Math.cos(Math.PI * i / numberOfFilters * (j - 0.5));
            }
            cepc.add(data);
         }
         resultList.add(cepc);
      }
      return resultList;
   }

   public double freqToMel(double frequency) {
      return 2595 * Math.log10(1 + frequency / 700);
   }

   public double melToFreq(double mel) {
      return 700 * (Math.pow(10, mel / 2595) - 1);
   }

   public double freqToBark(double frequency) {
      return 6 * Math.log((frequency / 600) + Math.sqrt(Math.pow(frequency / 600, 2) + 1));
   }

   public double barkToFreq(double bark) {
      return 600 * Math.sinh(bark / 6);
   }

   public List<Double> magnitudeSpectrum(Complex[] fftFrameComplex) {
      List<Double> fftFrame = new ArrayList<Double>();
      for (Complex complexFrame : fftFrameComplex) {
         double magSpect = Math.sqrt(Math.pow(complexFrame.getReal(), 2) + Math.pow(complexFrame.getImaginary(), 2));
         fftFrame.add(magSpect);
      }
      return fftFrame;
   }
}
